Skip to content

FIX: cursor.execute() with reset_cursor=False raises Invalid cursor state #521

Merged
jahnvi480 merged 5 commits intomainfrom
jahnvi/reset_cursor_error
Apr 21, 2026
Merged

FIX: cursor.execute() with reset_cursor=False raises Invalid cursor state #521
jahnvi480 merged 5 commits intomainfrom
jahnvi/reset_cursor_error

Conversation

@jahnvi480
Copy link
Copy Markdown
Contributor

@jahnvi480 jahnvi480 commented Apr 14, 2026

Work Item / Issue Reference

AB#44009

GitHub Issue: #506


Summary

This pull request improves the handling of cursor state when executing prepared statements with reset_cursor=False, ensuring that the prepared plan is correctly reused and the cursor state is properly managed between executions. It introduces a new close_cursor method at the ODBC statement handle level, updates the Python bindings, and adds comprehensive tests to verify the new behavior.

Enhancements to cursor state management:

  • Added logic in cursor.py to call hstmt.close_cursor() (which issues an ODBC SQLFreeStmt(SQL_CLOSE)) when reset_cursor=False, allowing prepared statements to be reused safely without resetting the entire cursor state. This resolves issues with invalid cursor state on re-execution.

ODBC handle and Python bindings updates:

  • Implemented the close_cursor method in the SqlHandle C++ class, which closes only the cursor on the statement handle without freeing the prepared statement.
  • Exposed the new close_cursor method to Python via the ddbc_bindings Pybind11 module, allowing it to be called from Python code.

Testing improvements:

  • Added multiple test cases in test_004_cursor.py to verify that reset_cursor=False correctly reuses prepared plans, returns new results, works across multiple iterations, handles queries without parameters, and functions correctly when the previous result set was not fully consumed.

…tate #506

Add SqlHandle::close_cursor() which calls SQLFreeStmt(SQL_CLOSE) to close
the ODBC cursor without destroying the prepared statement handle. When
reset_cursor=False, execute() now calls close_cursor() instead of
skipping cleanup entirely, allowing the prepared plan to be reused.

Added 5 tests covering reset_cursor=False scenarios.
Copilot AI review requested due to automatic review settings April 14, 2026 09:22
@github-actions github-actions Bot added the pr-size: medium Moderate update size label Apr 14, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 14, 2026

📊 Code Coverage Report

🔥 Diff Coverage

60%


🎯 Overall Coverage

79%


📈 Total Lines Covered: 6718 out of 8477
📁 Project: mssql-python


Diff Coverage

Diff: main...HEAD, staged and unstaged changes

  • mssql_python/cursor.py (100%)
  • mssql_python/pybind/ddbc_bindings.cpp (52.9%): Missing lines 1367-1368,1370-1371,1373-1374,1377-1378

Summary

  • Total: 20 lines
  • Missing: 8 lines
  • Coverage: 60%

mssql_python/pybind/ddbc_bindings.cpp

Lines 1363-1382

  1363 }
  1364 
  1365 void SqlHandle::close_cursor() {
  1366     if (_type != SQL_HANDLE_STMT || !_handle) {
! 1367         return;
! 1368     }
  1369     if (_implicitly_freed) {
! 1370         return;
! 1371     }
  1372     if (!SQLFreeStmt_ptr) {
! 1373         ThrowStdException("SQLFreeStmt function not loaded");
! 1374     }
  1375     SQLRETURN ret = SQLFreeStmt_ptr(_handle, SQL_CLOSE);
  1376     if (ret != SQL_SUCCESS && ret != SQL_SUCCESS_WITH_INFO) {
! 1377         ThrowStdException("SQLFreeStmt(SQL_CLOSE) failed");
! 1378     }
  1379 }
  1380 
  1381 SQLRETURN SQLGetTypeInfo_Wrapper(SqlHandlePtr StatementHandle, SQLSMALLINT DataType) {
  1382     if (!SQLGetTypeInfo_ptr) {


📋 Files Needing Attention

📉 Files with overall lowest coverage (click to expand)
mssql_python.pybind.logger_bridge.cpp: 59.2%
mssql_python.pybind.ddbc_bindings.h: 67.8%
mssql_python.row.py: 70.5%
mssql_python.pybind.logger_bridge.hpp: 70.8%
mssql_python.pybind.ddbc_bindings.cpp: 74.4%
mssql_python.pybind.connection.connection.cpp: 75.8%
mssql_python.__init__.py: 77.3%
mssql_python.ddbc_bindings.py: 79.6%
mssql_python.pybind.connection.connection_pool.cpp: 79.6%
mssql_python.connection.py: 85.2%

🔗 Quick Links

⚙️ Build Summary 📋 Coverage Details

View Azure DevOps Build

Browse Full Coverage Report

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Fixes Invalid cursor state errors when re-executing statements with reset_cursor=False by closing only the ODBC cursor (not the prepared statement), enabling safe prepared-plan reuse across executions.

Changes:

  • Add SqlHandle::close_cursor() (ODBC SQLFreeStmt(SQL_CLOSE)) and expose it via Pybind11.
  • Update cursor.execute() to close the ODBC cursor when reset_cursor=False.
  • Add regression tests covering re-execution, multi-iteration reuse, no-params execution, and re-execution without fully consuming prior results.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
tests/test_004_cursor.py Adds regression tests validating reset_cursor=False prepared-plan reuse and cursor state handling.
mssql_python/pybind/ddbc_bindings.h Declares SqlHandle::close_cursor() API.
mssql_python/pybind/ddbc_bindings.cpp Implements close_cursor() and exposes it to Python via Pybind11.
mssql_python/cursor.py Calls hstmt.close_cursor() when reset_cursor=False to prevent invalid cursor state on re-exec.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread mssql_python/pybind/ddbc_bindings.cpp Outdated
Comment thread tests/test_004_cursor.py
Copy link
Copy Markdown
Collaborator

@bewithgaurav bewithgaurav left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we're proposing to revamp the full reset cursor logic in our upcoming perf PRs
the complete hstmt deletion and creation should eventually be discarded & we will move to cursor resetting by default - considering the scope of this PR, this looks good.
requesting a change to add a safety guard for implicit freeing post conn deletion.
else lgtm

Comment thread mssql_python/pybind/ddbc_bindings.cpp Outdated
Comment thread mssql_python/pybind/ddbc_bindings.cpp
…rd, hide from public API

- Throw when SQLFreeStmt_ptr is not loaded instead of silent no-op
- Check SQLRETURN and throw on failure (allow SQL_SUCCESS_WITH_INFO)
- Add _implicitly_freed guard to prevent use-after-free
- Rename to _close_cursor (underscore prefix) to mark as internal
- Add assert row is not None before indexing in test
@jahnvi480 jahnvi480 merged commit 28e3b9f into main Apr 21, 2026
31 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr-size: medium Moderate update size

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants